iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 23
0
Modern Web

從巨人的 Tip 看 Angular系列 第 23

[Day 23] 深度探討 ng-content 背後的機制 (2)

  • 分享至 

  • xImage
  •  

好的,昨天用了一個簡單的範例來說明 ɵɵprojectionDef 以及 ɵɵprojection 這兩個 instructions,今天來用 multi ng-content 的範例來看一下 Angular 是怎麼進行 slot 分配的吧。

關於編譯後的程式碼

<app-mproject>
  <div footer>
    <p>FOOTER</p>
  </div>
  <div class="header">
    <p>HEADER</p>
  </div>
  <p>Not a footer is a p element</p>
  <span>Third</span>
  <p class="header" footer>Second</p>
</app-mproject>

↑ Block 1

const _c0 = [[["", 8, "header"]], [["", "footer", ""]], [["p"]], "*"];
const _c1 = [".header", "[footer]", "p", "*"];

↑ Block 2

MprojectComponent.ɵcmp = _angular_core__WEBPACK_IMPORTED_MODULE_0__[
  "ɵɵdefineComponent"
]({
  type: MprojectComponent,
  selectors: [["app-mproject"]],
  ngContentSelectors: _c1,
  // ... 略
  template: function MprojectComponent_Template(rf, ctx) {
    if (rf & 1) {
      _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵprojectionDef"](_c0);
      _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](0, "div", 0);
      _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵprojection"](1);
      _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](2, "div", 1);
      _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](3, "p");
      _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtext"](
        4,
        "mproject works!"
      );
      _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"]();
      _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"]();
      _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵprojection"](5, 1);
      _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"]();
      _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵprojection"](6, 2);
      _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵprojection"](7, 3);
    }
  },
  // ... 略
});

↑ Block 3

Block 2 的程式碼定義了兩個 const 變數,一個是 _c0,是之後要傳進 ɵɵprojectionDef 的 projectionSlots 參數,另一個 _c1 則是被放在 ngContentSelectors 屬性內。

關於 Angular 原始碼的部分

昨天我們看了 ɵɵprojectionDef 這個 instruction,今天要更仔細地往底層看,看一下同個頁面有多個 ng-content 的時候,Angular 會怎麼進行處理。

const projectionHeads: (TNode|null)[] = componentNode.projection =
        newArray(numProjectionSlots, null! as TNode);
const tails: (TNode|null)[] = projectionHeads.slice();

↑ Block 3

在 ɵɵprojectionDef 中這兩個 array 佔有很大的地位,首先 projectionHeads 與 componentNode.projection 是同一個陣列,各位應該都沒有問題,每當我們針對 projectionHeads 操作時,componentNode.projection 也會跟著有反應。

但 tails 的存在就有點特別了,因為他是由 projectionHeads 透過 slice 方法產生的另一個新的陣列,乍看下所有對 tail 的操作都不會反應給外不知道,那麼這個陣列是拿來做什麼的呢?這部分待會就會說明。

當進到 while 迴圈後,因為傳入的 projectionSlots 不再是 undefined,所以 Angular 會呼叫 matchingProjectionSlotIndex 這個函式,來協助取得該元素可以被投影的 slot 的 index:

const slotIndex =
      projectionSlots ? matchingProjectionSlotIndex(componentChild, projectionSlots) : 0;

↑ Block 4

export function matchingProjectionSlotIndex(tNode: TNode, projectionSlots: ProjectionSlots): number|
    null {
  let wildcardNgContentIndex = null;
  const ngProjectAsAttrVal = getProjectAsAttrValue(tNode);
  // ... 下接 Block 7
}

↑ Block 5

進到 matchingProjectionSlotIndex 之後,Angular 會先透過 getProjectAsAttrValue 這個函式,來尋找傳進來的 tNode 身上有沒有 projectAs 這個 attribute,
projectAs 這個 attribute 不在今天文章的討論範圍,所以這個函式就也先不講,同時因為我們的範例程式中也沒有使用到這個 attribute,所以 getProjectAsAttrValue 函式會回傳一個 null。
接著就開始針對 projectionSlots 內的 slot 與 tNode(componentChild)做比對啦。

先回憶一下傳入的 projectionSlots,也就是最開頭 Block 2 提到的 _c0 變數:

const _c0 = [
  [["", 8, "header"]], 
  [["", "footer", ""]], 
  [["p"]], 
  "*"
];

↑ Block 6

projectionSlots 的型別是一個 CssSelectorList 陣列或是 *,它會長得這麼特殊是因為已經被 Angular compiler 處理過了,有興趣的可以到 packages/core/src/render3/interfaces/projection.ts 這個檔案去一探究竟喔!

讓我們回到比對的部分:

for (let i = 0; i < projectionSlots.length; i++) {
    const slotValue = projectionSlots[i];
    // The last wildcard projection slot should match all nodes which aren't matching
    // any selector. This is necessary to be backwards compatible with view engine.
    if (slotValue === '*') {
      wildcardNgContentIndex = i;
      continue;
    }
    // If we ran into an `ngProjectAs` attribute, we should match its parsed selector
    // to the list of selectors, otherwise we fall back to matching against the node.
    if (ngProjectAsAttrVal === null ?
            isNodeMatchingSelectorList(tNode, slotValue, /* isProjectionMode */ true) :
            isSelectorInSelectorList(ngProjectAsAttrVal, slotValue)) {
      return i;  // first matching selector "captures" a given node
    }
  }
  return wildcardNgContentIndex;

↑ Block 7

進到比對 projectionSlots 的階段後,若馬上就遇到一個是 * 的預設 ng-content 的話,Angular 會先記錄這個 * slot 的 index,對於 Angular 的設計來說,這個 * slot 應該要是最後才選擇的。所以紀錄之後,會再往下遍歷剩下的 slot,直到沒有其他更合適的 slot 才會回傳 * slot 的 index。

來看一下 isNodeMatchingSelectorList 函式,它會透過呼叫 isNodeMatchingSelector 來檢查傳入的 tNode 是否有符合傳入 selector,然後回傳一個 boolean:

export function isNodeMatchingSelectorList(
    tNode: TNode, selector: CssSelectorList, isProjectionMode: boolean = false): boolean {
  for (let i = 0; i < selector.length; i++) {
    if (isNodeMatchingSelector(tNode, selector[i], isProjectionMode)) {
      return true;
    }
  }
  return false;

↑ Block 8

如果有符合的話,Angular 就會直接回傳當前 slot 的 index。

然後就可以進入下一個階段,將 componentChild 放入 tails 與 projectionHeads:

if (slotIndex !== null) {
  if (tails[slotIndex]) {
    tails[slotIndex]!.projectionNext = componentChild;
  } else {
    projectionHeads[slotIndex] = componentChild;
  }
  tails[slotIndex] = componentChild;
}

componentChild = componentChild.next;

↑ Block 9

首先當 tails[slotIndex] 內沒有存 componentChild 的話,Angular 會先將這個 componentChild 存入 projectionHeads[soltIndex] ,然後也會再將相同的 componentChild 存入 tails[soltIndex]

下次當有另一個 componentChild' 取得相同的 slotIndex 時,Angular 會將這個 componentChild' 存入 tails[slotIndex] 的 projectionNext 屬性,最後在存入 tails[slotIndex] 將它取代。

有看到這個設計的絕妙之處嗎?

Angular 團隊透過操作 projectionHeads 與 tails 兩個陣列,一方面可以用 projecitonHeads 來保留每一個 slot 最初遇到的 component,一方面透過 tails 來設定前一個 component 的 projectionNext 屬性、並持續更新 tails 為最後一個要放入 slot 的 component,就像是單向的 linked-list 一樣!

之後的故事我想大家都知道了!

那麼今天就這樣啦,祝各位另一個連假快樂!

最後一週啦 ?

以下按照入團順序列出我們團隊夥伴的系列文章!

請自由參閱 ?


上一篇
[Day 22] 深度探討 ng-content 背後的機制 (1)
下一篇
[Day 24] 簡單看看 @HostBinding decorator
系列文
從巨人的 Tip 看 Angular30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言